package com.tjbank.cssys.tools.redis;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import javax.annotation.Resource;
import java.time.Duration;
import java.util.*;

/**
 * Copyright © 天阳宏业科技股份有限公司 - All Rights Reserved
 *
 * @author guzheng@tansun.com.cn
 * @description:
 * @date: 2020-05-07 17:32
 **/
@EnableCaching
@Configuration
@AutoConfigureBefore(RedisAutoConfiguration.class)
@Slf4j
public class RedisConfiguration extends CachingConfigurerSupport {

    @Value("${spring.application.name:unnamespace}")
    private String cacheNamespace;

    /**
     * Key 默认过期时间: 1day = 86400s
     **/
    private Duration timeToLive = Duration.ofDays(1);

    @Resource
    private LettuceConnectionFactory lettuceConnectionFactory;

    private String[] ttls = {
        "TTL10s",
        "TTL20s",
        "TTL30s",
        "TTL60s",
        "TTL180s",
        "TTL300s"
    };

    private String[] getDefinedTtls() {
        return new String[]{
            cacheNamespace.concat(":").concat("TTL10s"),
            cacheNamespace.concat(":").concat("TTL20s"),
            cacheNamespace.concat(":").concat("TTL30s"),
            cacheNamespace.concat(":").concat("TTL60s"),
            cacheNamespace.concat(":").concat("TTL180s"),
            cacheNamespace.concat(":").concat("TTL300s")
        };
    }

    /**
     * redis key生成策略.
     * <p>
     * 该方法只是声明了key的生成策略,还未被使用,需在@Cacheable注解中指定keyGenerator.
     * 如: @Cacheable(value = "key", keyGenerator = "keyGenerator")
     * </p>
     * target: 类
     * method: 方法
     * params: 参数
     *
     * @return KeyGenerator
     */
    @Override
    @Bean
    public KeyGenerator keyGenerator() {
        return (target, method, params) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getSimpleName());
            sb.append("_");
            sb.append(method.getName());
            //for (Object obj : params) {
            //    sb.append(obj.toString());
            //}
            // org.springframework.util.StringUtils.arrayToCommaDelimitedString(params);
            if (log.isDebugEnabled()) {
                log.debug(">>>生成Redis Key:{}", sb);
            }
            return sb.toString();
        };
    }

    /**
     * 缓存管理器
     *
     * @return 缓存管理器
     */
    @Override
    @Bean(name = "cacheManager")
    public CacheManager cacheManager() {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
            // 设置缓存的过期时间
            //.entryTtl(timeToLive)
            // 无该行代码，则Spring Cache 默认使用::用作命名空间的分隔符
            .computePrefixWith(cacheName -> cacheNamespace + ":" + cacheName + ":")
            // 设置Key序列化器
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(getKeySerializer()))
            // 设置Value序列化器
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(getValueSerializer()))
            // 不缓存null值
            .disableCachingNullValues();

//        RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.RedisCacheManagerBuilder
//            .fromConnectionFactory(lettuceConnectionFactory);

        //  Spring提供的在RedisCacheManager来统一管理Cache的TTL，
        //  这种集中式的管理其实是我赞同的方式，若让他分散在各个缓存注解上，
        //  反而非常不利于后期的维护管理~~~因此这种方式我也是推荐的
//        Set<String> cacheNames = new HashSet<>();
//        Collections.addAll(cacheNames, getDefinedTtls());

        RedisCacheManagerHandler redisCacheManagerHandler = new RedisCacheManagerHandler(
                                                                    RedisCacheWriter.nonLockingRedisCacheWriter(lettuceConnectionFactory),
                                                                    redisCacheConfiguration,
                                                                    getRedisCacheConfigurationMap());

//        RedisCacheManager redisCacheManager = builder.cacheDefaults(redisCacheConfiguration)
//            .initialCacheNames(cacheNames)
//            .withInitialCacheConfigurations(getRedisCacheConfigurationMap())
//            .build();
        log.info(">>>自定义Spring Cache Manager配置完成. ");
        return redisCacheManagerHandler;
    }

    private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>(16);

        for (String ttl : getDefinedTtls()) {
            int start = ttl.indexOf(":") + 3;
            int end = ttl.indexOf("s", start + 1);
            int seconds = Integer.parseInt(ttl.substring(start + 1, end));
            redisCacheConfigurationMap.put(ttl, this.getRedisCacheConfigurationWithTtl(seconds));
        }

        return redisCacheConfigurationMap;
    }

    private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer seconds) {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
            // 设置缓存的过期时间
            .entryTtl(Duration.ofSeconds(seconds))
            // 设置Key序列化器
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(getKeySerializer()))
            // 设置Value序列化器
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(getValueSerializer()))
            // 不缓存null值
            .disableCachingNullValues();

        if (log.isDebugEnabled()) {
            log.debug(">>>初始化自定义cache ttl:{}", seconds);
        }
        return redisCacheConfiguration;
    }

    private RedisSerializer<String> getKeySerializer() {
        // RedisSerializer<String> stringSerializer = new StringRedisSerializer();
        // RedisKeySerializer stringSerializer = new RedisKeySerializer();
        return new StringRedisSerializer();
    }

    private RedisSerializer<Object> getValueSerializer() {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(
            Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        return jackson2JsonRedisSerializer;
    }

    /**
     * RedisTemplate配置
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        // 配置redisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);

        // key序列化
        redisTemplate.setKeySerializer(getKeySerializer());
        // value序列化
        redisTemplate.setValueSerializer(getValueSerializer());
        // Hash key序列化
        redisTemplate.setHashKeySerializer(getKeySerializer());
        // Hash value序列化
        redisTemplate.setHashValueSerializer(getValueSerializer());

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}
